/**
 ****************************************************************************
 * Copyright (C) Marcelo F. Ochoa. All rights reserved.                      *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included  with this distribution in *
 * the LICENSE file.                                                         *
 */

package com.prism.oracle;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.sql.CallableStatement;
import java.sql.SQLException;
import java.sql.Types;
import java.util.Enumeration;
import java.util.Vector;

import javax.servlet.http.HttpServletRequest;

import org.apache.log4j.Logger;

import com.prism.CgiVars;
import com.prism.ConnInfo;
import com.prism.Content;
import com.prism.DBConnection;
import com.prism.DBPrism;
import com.prism.SPProc;
import com.prism.toolkit.JowaArray;

/**
 * This class implements a ConcreteProduct class of Abstract Factory patterm.
 * Define a product object (DBConnJava represent a connection to this DB) to be created by the corresponding concrete factory.
 * Implements the AbstractProduct Interface (DBConnection). Also, implements a ConcreteClass of TemplateMethod patterm.
 * Implements the primitive operations to carry out subclass-specific steps of the algorithm (DBConnection::doCall)
 * DBConnection::getGeneratedStream will be implemented by the subclass DBConnJava8i & DBConnJavaLite
 * due to resolv incompatibility problem of CLOB data type used to return the generated page
 */
public abstract class DBConnJava extends DBConnection {
    Logger log = Logger.getLogger(DBConnJava.class);
    public java.lang.String caseSensity;

    public DBConnJava() {
      super();
    }

    /**
     * Create a concrete DBConnection (DBConnJava). Find extra properties attributes of this connection and return a
     * concrete connection object.
     */
    public abstract DBConnection create(ConnInfo cc);

    /**
     * Concrete operation of Template Method pattern. Reset packages state if the connection is statefull
     * At this momment this operation isn't implement in Java Version
     * In the future must be replaced for a function which find classes that
     * implements StateFull interface with the operation resetState()
     */
    public void resetPackages() throws SQLException {
        CallableStatement cs;
        //System.out.println("Reset session");
        cs = sqlconn.prepareCall("{ call DBMS_Session.reset_package }");
        cs.executeUpdate();
        cs.close(); //don't wait for garbage collector
        if (log.isDebugEnabled())
          log.debug(".resetPackages - call DBMS_Session.reset_package - Done. '");
    }

    /** Checks for this Stored Procedure argName is in the URL call without match case and image point extension. */
    public String getCallerName(String argName, Enumeration real_args) {
        while (real_args.hasMoreElements()) {
            String argCallName = (String)real_args.nextElement();
            if (argName.equalsIgnoreCase(argCallName) ||
                (argCallName.indexOf(".") > 0 && argName.equalsIgnoreCase(argCallName.substring(0, argCallName.indexOf(".")))))
                    return argCallName;
        }
        return null;
    }

    /**
     * Finds wich Stored Procedure match with the actual call plp have a Hashtable with all overloaded procedures definition
     * each overloaded definition has a Vector with the argument list
     * parse all FORM input parameters and arrays
     * Calling with prepared calls
     * Handle Case Insensitive args
     * Ej:
     * http://server:port/dpls/java/example.print?a=b
     * http://server:port/dpls/java/example.print?A=b
     * make the same call to the procedure example.print('b')
     */
    private Vector findArgumentsMatch(Vector argDefinitionList, Enumeration real_args) {
        int count = 0, foundCount = 0;
        while (real_args.hasMoreElements()) {
            String argName = (String)real_args.nextElement();
            if (argName.indexOf(".") > 0)
              if (argName.toLowerCase().endsWith(".x")) { // only use .x definition
                // if argName is arg.x
                // and remove trailing extension
                argName = argName.substring(0, argName.indexOf("."));
              } else // skip .y definition
                 continue;
            count++;
            if (log.isDebugEnabled())
              log.debug(".findArgumentsMatch - looking for the argument '"+argName+"' at position "+count);
            Enumeration argList = argDefinitionList.elements();
            while (argList.hasMoreElements()) {
                String procArgName = (String)argList.nextElement();
                if (argName.equalsIgnoreCase(procArgName)) {
                    foundCount++;
                    log.debug(".findArgumentsMatch - found it");
                    break;
                }
            }
        }
        if (log.isDebugEnabled())
          log.debug(".findArgumentsMatch - expected number of arguments "+count+" - found in request "+foundCount);
        if (count == foundCount && count == argDefinitionList.size())
            return argDefinitionList;
        else
            return null;
    }

    /**
     * Get the JDBC call command syntax
     * call package.procedure_name
     * or
     * call anonymous_procedure
     * Also check for the DAD setting about the case sensitive behaviour
     * @param String servletname (url part of the command)
     * @return StringBuffer (with the JDBC syntax).
     */
    private StringBuffer getCommandCall(String servletname) {
        StringBuffer command;
        int dotindex = servletname.indexOf('.');
        String plpackage;
        String plprocedure;
        try {
            if (caseSensity.equalsIgnoreCase("upper"))
                plpackage = servletname.substring(0, dotindex).toUpperCase();
            else if (caseSensity.equalsIgnoreCase("lower"))
                plpackage = servletname.substring(0, dotindex).toLowerCase();
            else
                plpackage = "\"" + servletname.substring(0, dotindex) + "\"";
            if (caseSensity.equalsIgnoreCase("upper"))
                plprocedure = servletname.substring(dotindex + 1).toUpperCase();
            else if (caseSensity.equalsIgnoreCase("lower"))
                plprocedure = servletname.substring(dotindex + 1).toLowerCase();
            else
                plprocedure = "\"" + servletname.substring(dotindex + 1) + "\"";
        } catch (IndexOutOfBoundsException e) { // anonymous procedure
            plprocedure = "\"" + servletname + "\"";
            if (caseSensity.equalsIgnoreCase("upper"))
                plprocedure = plprocedure.toUpperCase();
            else if (caseSensity.equalsIgnoreCase("lower"))
                plprocedure = "\"" + plprocedure.toLowerCase() + "\"";
            plpackage = null;
        }
        if (plpackage == null)  // Main calling command anonymous procedure format
            command = new StringBuffer("call " + plprocedure );
        else  // Main calling command package.procedure format
            command = new StringBuffer("call " + plpackage + "." + plprocedure );
        return command;
    }
    
    /**
     * loop through the overloading list of this procedure
     * to return the best match occurrence.
     * @param plp (Diccionary of procedures cached by DBPrism)
     * @param req (HttpServlet request for getting the arguments)
     * @return Vector of argument list, null if no procedures found.
     * */
    private Vector getArgumentList(SPProc plp,
                                   HttpServletRequest req) throws SQLException {
        Enumeration overloadList = plp.getAll();
        Vector procArgList = null;
        while (overloadList.hasMoreElements()) {
            Vector args = (Vector)overloadList.nextElement();
            procArgList = findArgumentsMatch(args, req.getParameterNames());
            if (procArgList != null) break;
        }
        return procArgList;
    }
    
    /**
     * Make a String buffer with the JDBC syntax for defining the argument call
     * (?,?,?) or an empty String if no arguments where given.
     * @param procArgListIterator
     * @return String 
     */
    private String getArgumentString(Enumeration procArgListIterator) {
        StringBuffer args = new StringBuffer("(");
        while (procArgListIterator.hasMoreElements()) {
                procArgListIterator.nextElement();
                args.append("?,");
        }
        args = new StringBuffer(args.toString().substring(0, args.length() - 1));
        if (args.length() > 0)
            args.append(")");
        return args.toString();
    }
    
    /** Concrete operation of Template Method pattern.
     *  Call to the concrete Java Stored Procedure in the DB
     *  Implemented in Sub classes DBConnJava8i and DBConnJavaLite
     *  */
    public void doIt(HttpServletRequest req, String servletname) 
           throws SQLException, UnsupportedEncodingException {
        CallableStatement cs = null;
        StringBuffer command = new StringBuffer("{ ");
        // Build procedure call pkg.proc
        command.append(getCommandCall(servletname));
        // get procedures definition from cache
        SPProc plp = DBPrism.proccache.get(connInfo, servletname, sqlconn);
        // get argument list which match with actual call
        Vector procArgList = getArgumentList(plp,req);
        if (procArgList == null)
            throw new SQLException("No Java Stored Procedures match with this call request: '"+servletname+"'");
        // Build procedure call parameter by parameter (?,?,?)
        command.append(getArgumentString(procArgList.elements())).append(" }");
        if (log.isInfoEnabled())
          log.info(".doIt - command:\n"+command);
        // Exec procedure in DB
        cs = sqlconn.prepareCall(command.toString());
        // Build procedure call parameter by parameter
        try {
          executeUpdate(cs,procArgList.elements(),plp,req);
        } catch (SQLException e) {
          // do not make anything pass the exception as is. 
          throw e;          
        } finally {
          if (cs != null) {
             cs.close();
             cs = null;
          }        
        }
    }
    
    /** Abstract method of Template Method */
    public abstract void executeUpdate(CallableStatement cs, 
                                   Enumeration procArgListIterator,
                                   SPProc plp,
                                   HttpServletRequest req) 
        throws SQLException;

    /** Concrete operation of Template Method pattern. Pass CGI Enviroment to the DB */
    public void setCGIVars(HttpServletRequest req, String name, String pass) throws SQLException {
        if (log.isDebugEnabled())
          log.debug(".setCGIVars - entered.");
        String argValue;
        CallableStatement cs = sqlconn.prepareCall("{ ? = call Jowa.initialize() }");
        cs.registerOutParameter(1, Types.INTEGER);
        cs.executeUpdate();
        cs.close();
        if ((argValue = req.getRemoteAddr()) != null) {
            cs = sqlconn.prepareCall("{ call JowaSec.setClientIp('" + argValue + "') }");
            cs.executeUpdate();
            cs.close();
        }
        cs = sqlconn.prepareCall("{ call JowaSec.setUserId('" + ((name.equals("") && pass.equals("")) ? null : name) + "') }");
        cs.executeUpdate();
        cs.close();
        cs = sqlconn.prepareCall("{ call JowaSec.setPassword('" +
            ((name.equals("") && pass.equals("")) ? null : pass) + "') }");
        cs.executeUpdate();
        cs.close();
        CgiVars env = new CgiVars(req, this.connInfo, name, pass);
        JowaArray arrNames = new JowaArray(env.size);
        JowaArray arrValues = new JowaArray(env.size);
        for (int i = 0; i < env.size; i++) {
            arrNames.setValueAt(i, env.names[i]);
            arrValues.setValueAt(i, env.values[i]);
        }
        cs = sqlconn.prepareCall("{ call Jowa.setCgiVars(?,?) }");
        cs.setBytes(1, arrNames.getBytes());
        cs.setBytes(2, arrValues.getBytes());
        cs.executeUpdate();
        cs.close(); // don't wait for garbage collector
        if (log.isDebugEnabled())
          log.debug(".setCGIVars - end - ok.");
    }

    /** Concrete operation of Template Method pattern. Return generated page as StringReader */
    public abstract Content getGeneratedStream(HttpServletRequest req) throws IOException, SQLException, UnsupportedEncodingException;

    /**
     * Concrete operation of Template Method pattern. authMode specifies whether to enable custom authentication.
     * If specified, the application authenticates users in its own level
     * and not within the database level. This parameter can be set to one of the following values : none (Default)
     * global custom perPackage
     */
    public int doAuthorize(String authMode, String ppackage) throws SQLException {
        int authStatus;
        if (authMode.equalsIgnoreCase("none")) { // Authorization none
            // always return true
            authStatus = 1;
        } else {
            CallableStatement cs;
            if (authMode.equalsIgnoreCase("global")) { // Authorization global
                // In global authentication all executes are validates by the owa_init.authorize function
                cs = sqlconn.prepareCall("{?=call JowaInit.authorize() }");
            } else { // Authorization per package
                if (ppackage.equals("")) // if anonymous procedure, anonymous function authorize
                        cs = sqlconn.prepareCall("{?=call authorize() }");
                else // else call to function authorize on this package
                        cs = sqlconn.prepareCall("{?=call " + ppackage + ".authorize() }");
            }
            cs.registerOutParameter(1, Types.INTEGER);
            cs.executeUpdate();
            // Check if password is valid
            authStatus = cs.getInt(1);
            cs.close(); //don't wait for garbage collector
        }
        if (log.isDebugEnabled())
          log.debug(".doAuthorize - returning: "+authStatus);
        return authStatus;
    }

    /**
     * Concrete operation of Template Method pattern. Return the realms sent back to the browser if the authorization fail
     * This realms is set by calling to JowaSec.getProtectionRealm function
     */
    public String getRealm() throws SQLException {
        CallableStatement cs;
        // get authorization mode
        cs = sqlconn.prepareCall("{?=call JowaSec.getProtectionRealm() }");
        cs.registerOutParameter(1, Types.VARCHAR);
        cs.executeUpdate();
        // Get Atuhorizacion Realm
        String realm = cs.getString(1);
        cs.close(); //don't wait for garbage collector
        if (log.isDebugEnabled())
          log.debug(".getRealm - returning: "+realm);
        return realm;
    }
}
